/*
   Copyright (C) 2006 Kai Sterker <kaisterker@linuxgames.com>
   Adapted as C library 2009 Klaus Blindert <klaus.blindert@web.de>

   Originally part of the Adonthell Project http://adonthell.linuxgames.com.
   Part of the libABF project.

   libABF is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   This library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with this library; if not, write to the
   Free Software Foundation, Inc.,
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/


/**
 * @file abf_format_xml.c
 * @author Kai Sterker <kaisterker@linuxgames.com>
 * @author Klaus Blindert <klaus.blindert@web.de>
 *
 * @brief Read/write xml-formatted data files.
 */

#include "abf_format_xml.h"

#include <assert.h>
#include <libxml/parser.h>
#include <memory.h>
#include <strings.h>

/* for converting binary to ascii */
const char *Bin2Hex = "0123456789ABCDEF";

/* Name of xml data file root node */
#define XML_ROOT_NODE "Data"

typedef enum { UNDEF, DATA, LIST, PARAM } context_state;

/**
 * Context for the sax parser
 */
typedef struct 
{
    /**
     * create a new parser context
     * @param record the record to be loaded
     */


    /** Storage for parameters */
    abf_flat_t *Record;
    /** id attribute of current element */
    char* Id;
    /** value of primitive element */
    char* Value;
    /** type of current element */
    abf_data_type Type;
    /** current state of sax parser */
    u_int8 State;
} data_sax_context;

data_sax_context **context_stack = 0;
u_int32 context_stack_size = 0;
u_int32 context_stack_capacity = 0;

void push_data_sax_context(data_sax_context* context);

data_sax_context* create_data_sax_context (abf_flat_t *record)
{
    data_sax_context* context = (data_sax_context*) malloc (sizeof (data_sax_context));
    context->Id = 0;
    context->Record = record;
    context->State = UNDEF;

    push_data_sax_context(context);
    return context;
}

void destroy_data_sax_context(data_sax_context* context)
{
    if (context->Id)
        free(context->Id);
    free(context);
    context_stack_size -= 1;
}


void grow_context_stack()
{
    data_sax_context** _context_stack = context_stack;
    context_stack_capacity = context_stack_capacity ? (context_stack_capacity*2) : 5;
    context_stack = (data_sax_context**) malloc(context_stack_capacity * sizeof (data_sax_context*));

    bzero(context_stack, context_stack_capacity);
    if( context_stack_size )
    {
        memcpy(context_stack, _context_stack, context_stack_size*sizeof(data_sax_context*));
        free(_context_stack);
    }
}

void push_data_sax_context(data_sax_context* context)
{
    if (context_stack_size == context_stack_capacity)
        grow_context_stack();
    context_stack[context_stack_size] = context;
    ++context_stack_size;
}

// std::string data_sax_context::Checksum;

// safely convert string to unsigned integer
static u_int32 string_to_uint (const char* value, u_int32 max)
{
    char *end = NULL;
    u_int32 intval = (u_int32) strtoul (value, &end, 10);

    // parsing okay?
    if (*end == '\0')
    {
        // in range?
        if (intval > max)
        {
            fprintf(stderr, "*** string_to_uint: integer overflow: value '%i' > max '%i'!\n", intval, max);
            return max;
        }
    }
    else
    {
        // value is not an integer
        fprintf (stderr, "*** string_to_uint: Can't convert '%s' to unsigned integer!\n", value);
        return 0;
    }

    return intval;
}

// safely convert string to signed integer
static s_int32 string_to_sint (const char* value, s_int32 min, s_int32 max)
{
    char *end = NULL;
    s_int32 intval = (s_int32) strtol (value, &end, 10);

    // parsing okay?
    if (*end == '\0')
    {
        // in range?
        if (intval < min)
        {
            fprintf(stderr, "*** string_to_sint: integer underflow: value '%i' < min '%i'!\n", intval, min);
            return min;
        }

        if (intval > max)
        {
            fprintf(stderr, "*** string_to_sint: integer overflow: value '%i' > max '%i'!\n", intval, max);
            return max;
        }
    }
    else
    {
        // value is not an integer
        fprintf (stderr, "*** string_to_sint: Can't convert '%s' to signed integer!\n", value);
        return -1;
    }

    return intval;
}

// convert primitive params
static void param_to_value (const data_sax_context *context)
{
    const char *value = context->Value;
    /* printf("id:%s\n",context->Id); */
    switch (context->Type)
    {
/*        case ABF_T_BLOB:
        {
            u_int32 j = 0;
            u_int32 size = context->Value.size())/2;
            u_int8 *bin = new u_int8[size];

            while (*value != '\0')
            {
                bin[j] = (strchr(disk_writer_xml::Bin2Hex, *value) - disk_writer_xml::Bin2Hex) << 4;
                bin[j] |= strchr(disk_writer_xml::Bin2Hex, *(value+1)) - disk_writer_xml::Bin2Hex;

                value += 2;
                j++;
            }

            context->Record->put_block (context->Id, bin, size);
            delete[] bin;
            break;
        }
*/
        case ABF_T_BOOL:
        {
            abf_put_bool (context->Record, context->Id, string_to_sint (value, 0, 1) == 1);
            break;
        }
        case ABF_T_DOUBLE:
        {
            abf_put_double (context->Record, context->Id, strtod (value, NULL));
            break;
        }
        case ABF_T_FLOAT:
        {
            abf_put_float (context->Record, context->Id, strtod (value, NULL));
            break;
        }
        case ABF_T_CHAR:
        case ABF_T_SINT8:
        {
            abf_put_sint8 (context->Record, context->Id, (s_int8) string_to_sint (value, INT8_MIN, INT8_MAX));
            break;
        }
        case ABF_T_SINT16:
        {
            abf_put_sint16 (context->Record, context->Id, (s_int16) string_to_sint (value, INT16_MIN, INT16_MAX));
            break;
        }
        case ABF_T_SINT32:
        {
            abf_put_sint32 (context->Record, context->Id, (s_int32) string_to_sint (value, INT32_MIN, INT32_MAX));
            break;
        }
        case ABF_T_STRING:
        {
            abf_put_string (context->Record, context->Id, value);
            break;
        }
        case ABF_T_UINT8:
        {
            abf_put_uint8 (context->Record, context->Id, (u_int8) string_to_uint (value, UINT8_MAX));
            break;
        }
        case ABF_T_UINT16:
        {
            abf_put_uint16 (context->Record, context->Id, (u_int16) string_to_uint (value, UINT16_MAX));
            break;
        }
        case ABF_T_UINT32:
        {
            abf_put_uint32 (context->Record, context->Id, (u_int32) string_to_uint (value, UINT32_MAX));
            break;
        }
        default:
        {
            fprintf (stderr, "*** param_to_value: invalid type for value '%s'!\n", value);
            break;
        }
    }
//     free(context->Id);
}

/**
 * Called when an opening tag has been processed.
 * @param ctx the parser context
 * @param name The element name
 * @param atts Element attributes
 */
static void data_start_element (void *ctx, const xmlChar *name, const xmlChar **atts)
{
    u_int32 i;
    abf_flat_t *record = 0;
    data_sax_context *context = context_stack[context_stack_size-1];

    switch (context->State)
    {
        // root node
        case UNDEF:
        {
            // check for root node
            if (strcmp ((char*) name, XML_ROOT_NODE) == 0)
            {
                context->State = DATA;
                // get attribute "cs", if present
                if (atts != NULL)
                {
/*                    for (u_int32 i = 0; atts[i] != NULL; i += 2)
                    {
                        if (atts[i][0] == 'c' && atts[i][1] == 's')
                        {
                          context->Checksum = (char*) atts[i+1];
                          break;
                        }
                    }
*/
                }
            }
            else
            {
                fprintf (stderr, "*** data_start_element: expected <" XML_ROOT_NODE ">, but got <%s>!\n", (char*) name);
            }
            break;
        }
        // start reading list or parameter
        case DATA: // fall through
        case LIST:
        {
            // get type of element
            abf_data_type type = abf_flat_gettypeforname ((char*) name);

            // reset id
            if (context->Id)
                free(context->Id);
            context->Id = 0;

            // get attribute "id", if present
            if (atts != NULL)
            {
                for (i = 0; atts[i] != NULL; i += 2)
                {
                    if (atts[i][0] == 'i' && atts[i][1] == 'd')
                    {
                        // store checksum in the Id field
                        context->Id = strdup((char*) atts[i+1]);
                        break;
                    }
                }
            }

            // set type of parameter
            context->Type = type;

            // check whether we got list
            switch (type)
            {
              // error
              case ABF_T_UNKNOWN:
              {
                return;
              }
              // list type
              case ABF_T_FLAT:
              {
                record = abf_create();

                /* create child context for sublist */
                /* printf("push:%d\n",context_stack_size); */
                context = create_data_sax_context (record);
                context->State = LIST;
                break;
              }
              // primitive type
              default:
              {
                  context->Value = 0;
                  context->State = PARAM;
                  break;
              }
            }
            break;
        }
        // error
        default:
        {
            fprintf (stderr, "*** data_start_element: entering <%s> with invalid state!\n", (char*) name);
            break;
        }
    }
}

/**
 * Called when the end of an element has been detected.
 * @param ctx the parser context
 * @param name the element name
 */
static void data_end_element (void *ctx, const xmlChar *name)
{
    data_sax_context *parent = 0;
    data_sax_context *context = context_stack[context_stack_size-1];

    switch (context->State)
    {
        // finished reading parameter
        case PARAM:
        {
          // convert and add value of param just read
            param_to_value (context);
            free(context->Value);
            context->Value = 0;
            context->State = LIST;
            break;
        }
        // finished reading list
        case LIST:
        {
            // this could also be </Data>, in which case we do nothing
            if (strcmp ((char*) name, XML_ROOT_NODE) != 0)
            {
                // get parent context
                assert (context_stack_size>1);
                parent = context_stack[context_stack_size-2];
                assert (parent);

                // add completed list to parent list
                abf_put_flat (parent->Record, parent->Id, context->Record);

                // cleanup
                abf_destroy(context->Record);
                destroy_data_sax_context(context);
//                 printf("pop:%d\n",context_stack_size);
            }
            break;
        }
        // finished reading configuration
        case DATA:
        {
            context->State = UNDEF;
            break;
        }
        // error
        default:
        {
            fprintf (stderr, "*** data_end_element: leaving </%s> with invalid state!\n", (char*) name);
            break;
        }
    }
}

/**
 * Receiving some chars from the parser.
 * @param ctx the parser context
 * @param content content of xml tag
 * @param len length of content
 */
static void data_read_characters (void *ctx, const xmlChar *content, int len)
{
    char* buffer;
    data_sax_context *context = context_stack[context_stack_size-1];

    // only read characters if we're inside a primitive type
    if (context->State == PARAM)
    {
        // store value first and assign when closing element, as
        // 'data_read_characters' is not called for empty elements.
        buffer = (char*) malloc(len+1);
        memcpy(buffer,content,len);
        buffer[len]='\0';
        context->Value = buffer;
    }
}

/**
 * Display and format error messages, gives file, line, position and
 * extra parameters.
 * @param ctx the parser context
 * @param msg error message to display
 * @param ... extra parameters for the message display
 */
static void data_parse_error (void *ctx, const char *msg, ...)
{
    va_list args;
    fprintf (stderr, "*** data_reader: ");
    va_start (args, msg);
    vfprintf (stderr, msg, args);
    va_end (args);
}

/**
 * Callbacks for the sax parser
 */
xmlSAXHandler data_sax_handler = {
    NULL, /* internalSubset */
    NULL, /* isStandalone */
    NULL, /* hasInternalSubset */
    NULL, /* hasExternalSubset */
    NULL, /* resolveEntity */
    NULL, /* getEntity */
    NULL, /* entityDecl */
    NULL, /* notationDecl */
    NULL, /* attributeDecl */
    NULL, /* elementDecl */
    NULL, /* unparsedEntityDecl */
    NULL, /* setDocumentLocator */
    NULL, /* startDocument */
    NULL, /* endDocument */
    data_start_element,
    data_end_element,
    NULL, /* reference */
    data_read_characters,
    NULL, /* ignorableWhitespace */
    NULL, /* processingInstruction */
    NULL, /* parseComment */
    NULL, /* xmlParserWarning */
    data_parse_error,
    data_parse_error,
    NULL, /* getParameterEntity */
    NULL, /* cdataBlock; */
    NULL, /* externalSubset; */
    XML_SAX2_MAGIC,
    NULL,
    NULL, /* startElementNsDebug */
    NULL, /* endElementNsDebug */
    NULL
};


// 
// // read record from XML file
// bool disk_writer_xml::get_state (const std::string & name, base::flat & data) const
// {
//     // find file
//     std::string file = name;
//     if (!base::Paths.find_in_path (file))
//     {
//         fprintf (stderr, "*** disk_writer_xml::get_state: cannot open '%s' for reading!\n", name.c_str());
//         return false;
//     }
// 
//     // clear contents of data
//     data.clear ();
// 
//   // prepare context
//     data_sax_context ctx (&data);
// 
//     // read data
//   if (xmlSAXUserParseFile (&data_sax_handler, &ctx, file.c_str ()) != 0)
//     {
//         fprintf (stderr, "*** disk_writer_xml::get_state: errors while parsing '%s'!\n", name.c_str ());
//         return false;
//     }
// 
//     /* Ignore checksum in XML files, since (a) we may edit them and (b) checksum will differ for
//        Big Endian and Little Endian systems.
// 
//     // compare checksum
//     std::ostringstream checksum;
//     checksum << (std::hex) << data.checksum ();
//     if (checksum.str () != ctx.Checksum)
//     {
//         fprintf (stderr, "*** disk_writer_xml::get_state: checksum mismatch in file '%s'.\n    Data might be corrupt.\n", name.c_str());
//         return false;
//     }
//     */
// 
//     return true;
// }

// recursively convert records to XML structure
void record_to_xml (abf_flat_t* record, xmlNodePtr parent)
{
    char *name;
    xmlNodePtr node;
    abf_data_type type;
    abf_flat_data_t* current;
    abf_flat_t* list;
    const char *str_type;

    // reset record iterator
    abf_flat_seek(record, 0);

    // add record data
    for(; (current = abf_flat_current(record)); abf_flat_advance(record))
    {
        type = abf_flat_gettype(current);
        str_type = abf_flat_gettypename(type);
        name = abf_flat_getname(current);

        // primitive type?
        if (type != ABF_T_FLAT)
        {
          // convert primitive value to xml char string
          const xmlChar* str_val = (const xmlChar*) abf_flat_tostring(current);
          node = xmlNewTextChild (parent, NULL, (const xmlChar *) str_type, str_val);
        }
        else
        {
            // we've got a list --> recurse
            list = abf_flat_toflat(current);
            node = xmlNewTextChild (parent, NULL, (const xmlChar *) str_type, NULL);
            record_to_xml (list, node);
            abf_destroy (list);
        }

        // add name attribute to node
        if (name != NULL && strlen (name) != 0)
        {
            xmlSetProp (node, (const xmlChar *) "id", (const xmlChar *) name);
        }
    }
}

// save record to XML file
void abf_write_xml (abf_flat_t* flat, const char* name)
{
    int retval = -1;
    // create empty xml document
    xmlDocPtr document = xmlNewDoc ((const xmlChar *) "1.0");

    // create root node
    document->children = xmlNewDocNode (document, NULL, (const xmlChar *) XML_ROOT_NODE, NULL);

  // recursively write content
    record_to_xml (flat, document->children);

    // add checksum as attribute of root node
//     std::ostringstream checksum;
//     checksum << (std::hex) << data.checksum ();
//     xmlSetProp (document->children, (const xmlChar *) "cs", (xmlChar*) checksum.str().c_str());

    // write document to file
    retval = xmlSaveFormatFile (name, document, 1);

    // cleanup
    xmlFreeDoc (document);

    // whether saving succeeded or not
//     return retval != -1;
    free(context_stack);
    context_stack = 0;
    context_stack_size = context_stack_capacity = 0;
}

abf_flat_t* abf_read_xml (const char* name)
{
    // find file
//     std::string file = name;
//     if (!base::Paths.find_in_path (file))
//     {
//         fprintf (stderr, "*** disk_writer_xml::get_state: cannot open '%s' for reading!\n", name.c_str());
//         return false;
//     }
    abf_flat_t* flat = abf_create();
    data_sax_context* context = create_data_sax_context(flat);

  // prepare context
//     data_sax_context ctx (&data);

    // read data
    if (xmlSAXUserParseFile (&data_sax_handler, &context, name) != 0)
    {
        fprintf (stderr, "*** disk_writer_xml::get_state: errors while parsing '%s'!\n", name);
        abf_destroy (flat);
        destroy_data_sax_context (context);
        return 0;
    }

    /* Ignore checksum in XML files, since (a) we may edit them and (b) checksum will differ for
       Big Endian and Little Endian systems.

    // compare checksum
    std::ostringstream checksum;
    checksum << (std::hex) << data.checksum ();
    if (checksum.str () != ctx.Checksum)
    {
        fprintf (stderr, "*** disk_writer_xml::get_state: checksum mismatch in file '%s'.\n    Data might be corrupt.\n", name.c_str());
        return false;
    }
    */
    destroy_data_sax_context (context);
    free(context_stack);
    context_stack = 0;
    context_stack_size = context_stack_capacity = 0;
    return flat;
}
